/*
 *  NFCSigning - Open source library for signing/validation of NDEF messages
 *  Copyright (C) 2009-2010 The NFCSigning Team
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 *
 */
package org.nfcsigning.certificate;

import org.nfcsigning.algorithm.DSAVerifier;
import org.nfcsigning.algorithm.PublicKey;
import org.nfcsigning.algorithm.SignatureException;
import org.nfcsigning.algorithm.SignatureVerifier;
import org.nfcsigning.algorithm.VerifierFactory;
import org.nfcsigning.record.CertificateBytes;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.util.Calendar;
import java.util.Date;
import java.util.Enumeration;
import java.util.TimeZone;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.DERBitString;
import org.bouncycastle.asn1.DEREncodable;
import org.bouncycastle.asn1.DERNull;
import org.bouncycastle.asn1.DERObjectIdentifier;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.DSAParameter;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.asn1.x509.X509CertificateStructure;
import org.bouncycastle.asn1.x509.X509Extensions;
import org.bouncycastle.asn1.x509.X509Name;

/**
 *
 * @author Markus Kilås
 */
public class X509CertificateChain extends CertificateChain {

    private String lastErrorMessage;
    private X509Certificate[] certificates;
    private X509Certificate rootCertificate;

    public X509CertificateChain(CertificateBytes[] chainBytes) throws IOException {
        
        this.certificates = new X509Certificate[chainBytes.length];
        
        for(int i = 0; i < chainBytes.length; i++) {
            this.certificates[i] = new X509Certificate(chainBytes[i].getBytes());
        }
    }
    
    public X509CertificateChain(X509Certificate[] chain) {
        this.certificates = chain;
    }
    
    private X509CertificateChain(X509Certificate[] chain1, X509Certificate[] chain2) {
        this.certificates = new X509Certificate[chain1.length+chain2.length];
        System.arraycopy(chain1, 0, this.certificates, 0, chain1.length);
        System.arraycopy(chain2, 0, this.certificates, chain1.length, chain2.length);
    }

    public Certificate[] getCertificates() {
        return certificates;
    }
    
    public boolean validateChain(Certificate[] trustedCerts, VerifierFactory verifierFactory) throws InvalidCertificateChain {
        
        if(trustedCerts == null) {
            throw new IllegalArgumentException("trustedCerts must not be null");
        }

        // Find root certificate
        this.rootCertificate = findMatchingRootCertificate(certificates, trustedCerts);

        if(rootCertificate == null) {
            lastErrorMessage = "Untrusted certificate chain";
            return false;
        }
                
        for(int i = 0; i < certificates.length; i++) {

            X509Certificate cert = certificates[i];
            X509Certificate issuer = (i >= certificates.length - 1) ? rootCertificate : certificates[i + 1];
            X509CertificateStructure certStruct = cert.getStructure();
            X509CertificateStructure issuerStruct = issuer.getStructure();
            
            boolean isCA = i != 0;
            //System.out.println("Cert: " + cert.getStructure().getSubject().toString() + " is CA: " + isCA);

            try {
                // Check validity                
                Date startDate = toDate(certStruct.getStartDate().getTime());
                Date endDate = toDate(certStruct.getEndDate().getTime());     
                Date now = new Date();
                if(now.getTime() < startDate.getTime()) {
                    lastErrorMessage = "Certificate " + i + " not valid until " + startDate;
                    return false;
                }
                if(now.getTime() > endDate.getTime()) {
                    lastErrorMessage = "Certificate " + i + " expired " + endDate;
                    return false;
                }
                
                // Check that the next certificate in chain is the issuer
                if (!certStruct.getIssuer().equals(issuerStruct.getSubject())) {
                    lastErrorMessage = "Issuer for certifcate " + i + " not directly following it in chain";
                    return false;
                }

                // Check that it is the same signature algorithm in both certificate and TBS certificate
                if (!certStruct.getSignatureAlgorithm().equals(certStruct.getTBSCertificate().getSignature())) {
                    lastErrorMessage = "signature algorithm in TBS cert not same as outer cert";
                    return false;
                }

                // Check extensions in X.509 V3
                if(certStruct.getTBSCertificate().getVersion() == 0x03) {
                    X509Extensions exts = certStruct.getTBSCertificate().getExtensions();
                    Enumeration oids = exts.oids();
                    while(oids.hasMoreElements()) {
                        DERObjectIdentifier oid = (DERObjectIdentifier) oids.nextElement(); 

                        if(X509Extensions.BasicConstraints.equals(oid)) {
                            ASN1InputStream dis = new ASN1InputStream(new ByteArrayInputStream(exts.getExtension(oid).getValue().getOctets()));
                            BasicConstraints bc = new BasicConstraints((DERSequence)dis.readObject());

                            if (isCA && !bc.isCA()) {
                                lastErrorMessage = "Certificate not allowed for CA";
                                return false;
                            }
                            
                            BigInteger maxPathLen = bc.getPathLenConstraint();
                            if(maxPathLen != null) {
                                if(i >= maxPathLen.intValue()) {
                                    lastErrorMessage = "Length of certfificate chain constrained";
                                    return false;
                                }
                            }

                        } else if(X509Extensions.KeyUsage.equals(oid)) {

                            ASN1InputStream dis = new ASN1InputStream(new ByteArrayInputStream(exts.getExtension(oid).getValue().getOctets()));
                            DERBitString bits = (DERBitString) dis.readObject();
                            int ku = bits.intValue();

                            // digital signature
                            if(!isCA && (ku & KeyUsage.digitalSignature) == 0) {
                                lastErrorMessage = "Certificate not allowed for digital signatures";
                                return false;
                            }
                            
                            // Certificate signing
                            if(isCA && (ku & KeyUsage.keyCertSign) == 0) {
                                lastErrorMessage = "Certificate not allowed for cert signing";
                                return false;
                            }

                        } else if(exts.getExtension(oid).isCritical()) {
                            lastErrorMessage = "Critical unknown extension: " + oid;
                            return false;
                        }
                    }
                }

                // Validate signature
                byte[] signature = certStruct.getSignature().getBytes();
                byte[] plainText = certStruct.getTBSCertificate().getEncoded();

                SignatureVerifier verifier = verifierFactory.getVerifier(certStruct.getSignatureAlgorithm().getObjectId());
                if(verifier == null) {
                    lastErrorMessage = "Signature algorithm not supported on certificate " + i;
                    return false;
                }

                try {                   
                    if(!verifier.verifySignature(signature, plainText, getPublicKey(issuer))) {
                        lastErrorMessage = "Bad signature on certificate " + i;
                        return false;
                    }
                                       
                } catch (SignatureException ex) {
                    lastErrorMessage = "Verification of certificate " + i + " failed: " + ex.getMessage();
                    return false;
                }
                
            } catch (IOException ex) {
                ex.printStackTrace();
                throw new InvalidCertificateChain("IOException: " + ex.getMessage());
            }
        }
        return true;
    }

    public String getValidationMessage() {
        return lastErrorMessage;
    }
    
    static X509Certificate findMatchingRootCertificate(X509Certificate[] chain, Certificate[] possibleRoots) {
    
        if(chain.length < 1) {
            return null;
        }
        
        X509Name issuer = chain[chain.length-1].getStructure().getIssuer();
        
        for(int i = 0; i < possibleRoots.length; i++) {
            if(possibleRoots[i] instanceof X509Certificate) {
                if(issuer.equals(((X509Certificate)possibleRoots[i]).getStructure().getSubject())) {
                    return (X509Certificate) possibleRoots[i];
                }
            }
        }
        
        return null;
    }
    
    /**
     * 
     * Note: Might not handle daylight savings time correctly.
     * 
     * @param time
     * @return
     */
    static Date toDate(String time) {
        // Format: YYMMDDhhmmssGMT(+hh:mm|-hh:mm)
        // Example: "20091001123813GMT+00:00" 
        
        int year = Integer.parseInt(time.substring(0, 4));
        int month = Integer.parseInt(time.substring(4, 6));
        int day = Integer.parseInt(time.substring(6, 8));
        int hour = Integer.parseInt(time.substring(8, 10));
        int minute = Integer.parseInt(time.substring(10, 12));
        int second = Integer.parseInt(time.substring(12, 14));
        
        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        cal.set(Calendar.YEAR, year);
        cal.set(Calendar.MONTH, month-1);
        cal.set(Calendar.DAY_OF_MONTH, day);
        cal.set(Calendar.HOUR_OF_DAY, hour);
        cal.set(Calendar.MINUTE, minute);
        cal.set(Calendar.SECOND, second);
        cal.set(Calendar.MILLISECOND, 0);
        long timestamp = cal.getTime().getTime();
        
        int timeZoneHour = Integer.parseInt(time.substring(18, 20));
        int timeZoneMinute = Integer.parseInt(time.substring(21, 23));
        
        // If "+" we should subtract to obtain GMT
        if("+".equals(time.substring(17, 18))) {
            timeZoneHour *= -1;
            timeZoneMinute *= -1;
        }
        
        timestamp += (timeZoneHour*60 + timeZoneMinute) * 60 * 1000;
        
        return new Date(timestamp);
    }

    public Certificate getUserCertificate() {
        return certificates[0];
    }

    public Certificate getRootCertificate() {
        return rootCertificate;
    }

    public CertificateChain concat(CertificateChain chain) {
        if(!(chain instanceof X509CertificateChain)) {
            throw new IllegalArgumentException("Incompatible certificate chain types");
        }
        return new X509CertificateChain(certificates, (X509Certificate[])chain.getCertificates());
    }

    private static DEREncodable findInheritedDSAParams(int certIndex, X509Certificate[] certificates, X509Certificate lastCertificate) {
        for(int i = certIndex+1; i < certificates.length+1; i++) {

            X509Certificate cert = (i == certificates.length) ? lastCertificate : certificates[i];
            SubjectPublicKeyInfo pkInfo = cert.getSubjectPublicKeyInfo();

            if (DSAVerifier.id_dsa_with_sha1.equals(cert.getStructure().getSignatureAlgorithm().getObjectId()) &&
                    pkInfo.getAlgorithmId().getParameters() != null && pkInfo.getAlgorithmId().getParameters() != null) {

                DEREncodable de = pkInfo.getAlgorithmId().getParameters();

                if (de != null && !(de.getDERObject() instanceof DERNull)) {
                    return (DEREncodable) DSAParameter.getInstance(de.getDERObject());
                }
            }
        }
        return null;
    }

    public PublicKey getPublicKey(Certificate certificate) {
        
        if(certificate == rootCertificate) {
            return new PublicKey(certificate.getSubjectPublicKeyInfo());
        }
        
        int certIndex = getCertIndex(certificate);
        if(certIndex == -1 || !(certificate instanceof X509Certificate)) {   // Not found
            return null;
        }
        X509Certificate cert = (X509Certificate) certificate;

        PublicKey pubKey = new PublicKey(cert.getSubjectPublicKeyInfo());

        // DSA domain parameter inheritance
        if(DSAVerifier.id_dsa_with_sha1.equals(cert.getStructure().getSignatureAlgorithm().getObjectId())) {

            SubjectPublicKeyInfo pkInfo = cert.getSubjectPublicKeyInfo();

            if (pkInfo.getAlgorithmId().getParameters() == null || pkInfo.getAlgorithmId().getParameters() instanceof DERNull) {

                DEREncodable inheritedParams = findInheritedDSAParams(certIndex, certificates, rootCertificate);
                if(inheritedParams != null) {
                    pubKey = new PublicKey(cert.getSubjectPublicKeyInfo(), inheritedParams);
                }
            }
        }
        return pubKey;
    }

    private int getCertIndex(Certificate certificate) {
        for(int i = 0; i < certificates.length; i++) {
            if(certificates[i] == certificate) {
                return i;
            }
        }
        return -1;
    }

}
